using System.Text;
using System.Text.RegularExpressions;
using Cocona.Application;
using Cocona.Command;
using Cocona.ShellCompletion.Candidate;

namespace Cocona.ShellCompletion.Generators;

/// <summary>
/// Generates the shell completion code for Zsh.
/// </summary>
public class ZshCoconaShellCompletionCodeGenerator : ICoconaShellCompletionCodeGenerator
{
    private readonly string _appName;
    private readonly string _appCommandName;
    private readonly ICoconaCompletionCandidates _completionCandidates;

    public IReadOnlyList<string> Targets { get; } = new[] {"zsh"};

    public ZshCoconaShellCompletionCodeGenerator(
        ICoconaApplicationMetadataProvider applicationMetadataProvider,
        ICoconaCompletionCandidates completionCandidates
    )
    {
        _appName = Regex.Replace(applicationMetadataProvider.GetProductName(), "[^a-zA-Z0-9_]", "__");
        _appCommandName = applicationMetadataProvider.GetExecutableName();
        _completionCandidates = completionCandidates;
    }

    public void Generate(TextWriter writer, CommandCollection commandCollection)
    {
        writer.WriteLine($"#compdef {_appCommandName}");
        writer.WriteLine($"# ");
        writer.WriteLine($"# Generated by Cocona {nameof(ZshCoconaShellCompletionCodeGenerator)}");
        writer.WriteLine($"# ");
        writer.WriteLine();

        WriteRootCommandDefinition(writer, commandCollection);

        writer.WriteLine($"__cocona_{_appName}_onthefly() {{");
        writer.WriteLine($"    local -a items; items=(${{(f)\"$(\"${{exec_command}}\" --completion-candidates \"zsh:$1\" -- \"${{words[@]}}\")\"}})");
        writer.WriteLine($"    _describe 'items' items");
        writer.WriteLine($"}}");

        writer.WriteLine();
        writer.WriteLine($"_{_appCommandName}() {{");
        writer.WriteLine($"    __cocona_{_appName}_commands_root");
        writer.WriteLine($"}}");
    }

    public void GenerateOnTheFlyCandidates(TextWriter writer, IReadOnlyList<CompletionCandidateValue> values)
    {
        foreach (var value in values)
        {
            writer.WriteLine($"{value.Value}:{value.Description}");
        }
    }

    private void WriteRootCommandDefinition(TextWriter writer, CommandCollection commandCollection)
    {
        var subCommands = commandCollection.All.Where(x => !x.IsHidden && !x.IsPrimaryCommand).ToArray();

        writer.WriteLine($"__cocona_{_appName}_commands_root() {{");
        writer.WriteLine($"    local exec_command; exec_command=\"${{words[1]/#\\~/$HOME}}\"");
        if (commandCollection.Primary != null)
        {
            WriteZshArguments(writer, "root", commandCollection.Primary, subCommands);
        }
        writer.WriteLine("}");
        writer.WriteLine();

        // sub-commands
        foreach (var subCommand in subCommands)
        {
            WriteCommandDefinition(writer, $"root_{subCommand.Name}", subCommand);
        }
    }

    private void WriteCommandDefinition(TextWriter writer, string commandName, CommandDescriptor command)
    {
        var subCommands = command.SubCommands?.All.Where(x => !x.IsHidden && !x.IsPrimaryCommand).ToArray() ?? Array.Empty<CommandDescriptor>();

        writer.WriteLine($"__cocona_{_appName}_commands_{commandName}() {{");
        WriteZshArguments(writer, commandName, command, subCommands);
        writer.WriteLine("}");
        writer.WriteLine();

        foreach (var subCommand in subCommands)
        {
            WriteCommandDefinition(writer, $"{commandName}_{subCommand.Name}", subCommand);
        }
    }

    private void WriteZshArguments(TextWriter writer, string commandName, CommandDescriptor command, CommandDescriptor[] subCommands)
    {
        writer.WriteLine($"    local -a commands");
        writer.WriteLine($"    commands=(");
        foreach (var subCommand in subCommands)
        {
            writer.WriteLine($"        '{subCommand.Name}:{subCommand.Description}'");
        }
        writer.WriteLine($"    )");

        var helpOption = command.OptionLikeCommands.FirstOrDefault(x => x.Name == "help");

        writer.WriteLine($"    _arguments -n -s -S : \\");
        foreach (var option in command.Options.OfType<ICommandOptionDescriptor>().Concat(command.OptionLikeCommands).Where(x => !x.Flags.HasFlag(CommandOptionFlags.Hidden)))
        {
            if (option is CommandOptionDescriptor commandOption)
            {
                if (commandOption.OptionType == typeof(bool))
                {
                    writer.WriteLine($"        '{GetOptions(option, helpOption)}[{option.Description}]' \\");
                }
                else
                {
                    writer.WriteLine($"        '{GetOptions(option, helpOption)}[{option.Description}]: :{GetArgumentValues(commandOption)}' \\");
                }
            }
            else
            {
                writer.WriteLine($"        '{GetOptions(option, helpOption)}[{option.Description}]' \\");
            }
        }
        foreach (var arg in command.Arguments)
        {
            writer.WriteLine($"        '{arg.Order + 1}:{arg.Name}:{GetArgumentValues(arg)}' \\"); // NOTE: the argument index must begin at 1.
        }

        if (subCommands.Any())
        {
            writer.WriteLine($"         \"1: :{{_describe 'command' commands}}\" \\");
            writer.WriteLine($"        '*:: :->args'");

            writer.WriteLine();
            writer.WriteLine($"        case $state in");
            writer.WriteLine($"            args)");
            writer.WriteLine($"                case $words[1] in");
            foreach (var subCommand in subCommands)
            {
                writer.WriteLine($"                    {subCommand.Name}) __cocona_{_appName}_commands_{commandName}_{subCommand.Name};;");
            }
            writer.WriteLine($"                esac");
            writer.WriteLine($"                ;;");
            writer.WriteLine($"        esac");
        }
        else
        {
            writer.WriteLine($"        #");
        }
    }

    private string GetOptions(ICommandOptionDescriptor option, ICommandOptionDescriptor? helpOption)
    {
        var sb = new StringBuilder();

        // --help, -h options are exclusive options.
        if (helpOption != null && helpOption != option)
        {
            if (helpOption.ShortName.Any())
            {
                sb.Append($"(--{helpOption.Name} {string.Join(" ", helpOption.ShortName.Select(x => "-" + x))})");
            }
            else
            {
                sb.Append($"(--{helpOption.Name})");
            }
        }

        var isEnumerableLike = option is CommandOptionDescriptor commandOption && commandOption.IsEnumerableLike;
        if (option.ShortName.Any())
        {
            sb.Append($"{(isEnumerableLike ? "*" : "")}'{{--{option.Name},{string.Join(",", option.ShortName.Select(x => "-" + x))}}}'");
        }
        else
        {
            sb.Append($"{(isEnumerableLike ? "*" : "")}--{option.Name}");
        }

        return sb.ToString();
    }

    private string GetArgumentValues(CommandOptionDescriptor option)
    {
        return GetArgumentValuesCore(_completionCandidates.GetStaticCandidatesFromOption(option), option.Name);
    }
    private string GetArgumentValues(CommandArgumentDescriptor argument)
    {
        return GetArgumentValuesCore(_completionCandidates.GetStaticCandidatesFromArgument(argument), argument.Name);
    }

    private string GetArgumentValuesCore(StaticCompletionCandidates candidates, string name)
    {
        if (candidates.IsOnTheFly)
        {
            // NOTE: See "_arguments/specs: actions" of man 1 zshcompsys
            // "If the action starts with a space, the remaining list of words will be invoked unchanged."
            return $" __cocona_{_appName}_onthefly {name}";
        }
        else
        {
            return candidates.Result!.ResultType switch
            {
                CompletionCandidateResultType.Default
                    => "_files",
                CompletionCandidateResultType.File
                    => "_files",
                CompletionCandidateResultType.Directory
                    => "_path_files -/",
                CompletionCandidateResultType.Keywords
                    => $"({string.Join(" ", candidates.Result!.Values.Select(x => x.Value))})",
                _
                    => "_files",
            };
        }
    }
}